Skip to content

fix(data-objectstack): parse a write-strip's reason against the spec enum at the boundary - #6884

Merged
os-sam merged 4 commits into
mainfrom
claude/issue-4934-dropped-fields-reason-boundary
Aug 30, 2026
Merged

fix(data-objectstack): parse a write-strip's reason against the spec enum at the boundary#6884
os-sam merged 4 commits into
mainfrom
claude/issue-4934-dropped-fields-reason-boundary

Conversation

@claude

@claude claude Bot commented Aug 30, 2026

Copy link
Copy Markdown
Contributor

Part of #4934 — the boundary and the one consumer it moves, together. No
closing keyword:
the card closes when the fable-tier review lands this PR.

What changed — the boundary (@object-ui/data-objectstack)

notifyDroppedFields filtered a create/update response's droppedFields on
SHAPE alone — a hand-written e is DroppedFieldsEvent guard that checked
Array.isArray(fields) and nothing else. reason was never read, never compared
against DroppedFieldsEventSchema.shape.reason, and the predicate asserted the
entry into the spec type regardless. notifyBatchDroppedFields did the same
through entry as DroppedFieldsEvent & { index?: number }.

Both paths now parse reason against the enum the installed @objectstack/spec
pin declares, derived from DroppedFieldsEventSchema.shape.reason.options rather
than restated, so a pin bump that adds an arm widens the accept set on its own.

Per the maintainer ruling of 2026-08-22 (option A, confirming the triage
adjudication), all four parts:

  1. Validated at the boundary — both notifyDroppedFields and
    notifyBatchDroppedFields.
  2. Every entry is kept. Nothing is dropped; dropping would recreate the
    silence console 编辑表单对 readonlyWhen 只认一半:previous.* 口径 fail-open 致锁定字段可改且被静默丢弃;record.* 口径虽渲染成只读却仍被整表回传,触发「部分字段未保存」假告警 #3484 removed.
  3. An off-union reason gets a named, documented skew arm
    UnrecognizedDropReasonEvent, carrying UNRECOGNIZED_DROP_REASON plus the
    wire value verbatim in unrecognizedReason. It is not asserted into the
    union and the surface is not blanket-widened to string: the spec type stays
    the canonical arm (台账燃尽批次 6/8 · data-objectstack + plugin-chatbot + plugin-list,12 符号(objectstack#4115) #3160), and WriteWarningEvent['droppedFields'] becomes
    the two-arm DroppedFieldsNotice.
  4. Pinned by packages/data-objectstack/src/droppedFieldsReason.boundary.test.ts.

The shape filter's guard now asserts only what it actually checks
(WireDroppedFieldsEntry, whose reason is unknown) — that is where the
original lie lived, so it is deleted rather than worked around.

What changed — the consumer (@object-ui/app-shell)

The first round stopped and reported a fork, as the 2026-08-22 ruling required:
the sole consumer, packages/app-shell/src/providers/writeWarningToast.ts, kept
its runtime behaviour exactly but no longer compiled. That fork was ruled on
2026-08-30 (option A, comment 5468185263): the two annotations ride in this PR.
They are now applied — and nothing else in packages/app-shell.

-function lineFor(reason: DroppedFieldsEvent['reason']): StrippedLine {
+function lineFor(reason: DroppedFieldsNotice['reason']): StrippedLine {
-  const byReason = new Map<DroppedFieldsEvent['reason'], string[]>();
+  const byReason = new Map<DroppedFieldsNotice['reason'], string[]>();

This is not a widening — the file was already written for this value

writeWarningToast.ts handles the off-union reason at runtime on purpose,
and said so before this card existed. lineFor's body is a deliberately widened
lookup with a fallback:

function lineFor(reason: DroppedFieldsNotice['reason']): StrippedLine {
  const known: Partial<Record<string, StrippedLine>> = STRIPPED_LINE;
  return known[reason] ?? strippedLineUnknownReason;
}

and the docstring immediately above it says so in as many words — quoted verbatim
from the tree:

The LOOKUP is done through a widened view of the same table, because the
runtime value may sit outside that union (see strippedLineUnknownReason); the
undefined this branch handles is therefore reachable, not dead.

So this PR does not widen two annotations to admit a new case. It corrects
two annotations that were already narrower than the file's own documented
contract
. The author anticipated exactly this value and wrote
strippedLineUnknownReason for it; only the parameter and the Map key had been
left pinned to the spec union.

STRIPPED_LINE keeps its Record declaration over the SPEC union — line 108
after this diff, unmoved. That is #3935's guarantee that a new SPEC arm fails
type-check unworded, and it is why the annotations name
DroppedFieldsNotice['reason'] rather than string. Widen the lookups, not the
table.

⚠️ Clause ② FIRES — this PR's review is fable-tier

The first round's clause-② self-check concluded "does not fire" on the grounds
that nothing was added to the published @objectstack/spec type. That reading is
too narrow: it scopes "public" to the spec's surface, and the gate does not.
Clause ② is judged from card CONTENT, and this card does both of the things it
names, on objectui's own published surface:

  • it changes contract accept/reject behaviour — the boundary now parses
    reason where it previously asserted it; and
  • it widens the public surfaceWriteWarningEvent['droppedFields'] goes
    from DroppedFieldsEvent[] to the two-arm DroppedFieldsNotice[], which is
    exactly what broke a consumer.

This PR therefore stays in draft for a fable-tier review.

The narrow part of the original check is still true and still worth stating:
nothing was added to the published @objectstack/spec type, DroppedFieldsEvent
is re-exported untouched, and spec-symbol-batch6.test.ts's pin that it IS the
spec type still passes.

Verification

All readings in this section are on head c01e5a2 (the previous round), re-run
after that round's last commit. This round's readings are on 3ec17deb, above.

Run Reading
pnpm --filter @object-ui/app-shell type-check echoed tsc --noEmit && tsc -p tsconfig.test.json, exit 0
pnpm exec turbo run build --filter='!@object-ui/site' --concurrency=2 Tasks: 43 successful, 43 totalzero failures
pnpm exec vitest run packages/app-shell/src/providers/writeWarningToast.test.ts Tests 13 passed (13)
pnpm exec vitest run packages/data-objectstack/ Test Files 49 passed (49) / Tests 669 passed (669) — unmoved by this round
pnpm --filter @object-ui/app-shell lint 2798 problems (0 errors, 2798 warnings), exit 0
check:control-bytes · spec-symbols · phantom-deps · self-import · esm-specifiers · readme-exports · spec-floors all exit 0
check-changeset-no-major · -presence · -fixed · -overwrite all exit 0
check:i18n-keys · i18n-drift · i18n-dead-keys all exit 0

The build reading is 43 of 43, not "40 of 40". The first round measured
Tasks: 39 successful, 40 total with @object-ui/app-shell#build failing — and
that denominator existed because it was failing. app-shell#build has three
direct dependents in the turbo graph (@object-ui/console,
@object-ui/example-console-starter, @object-ui/example-byo-backend-console);
when it fails they are never attempted and never counted. With app-shell green
they run, and the total is 43. Three more packages build than were even attempted
before.

check:readme-exports and check:spec-floors read dist. Their readings here
are post-build and real, not no-artifact: readme-exports
3282 export symbol(s) read from 37 of 40 package(s) ... 0 unbuilt, spec-floors
274 (subpath, symbol) pair(s) judged. @object-ui/data-objectstack was rebuilt
before app-shell read it and the marker was confirmed to have reached the
artifact (grep -c UNRECOGNIZED_DROP_REASON dist/index.d.ts dist/index.js → 5, 3).

check-changeset-overwrite and the three i18n gates are not in the dispatched
clue list
— they were derived from the actual diff. This round edits an
existing .changeset/*.md, and writeWarningToast.ts is a wording table whose
own docstring says the i18n gate is watching it. Both families are green.

Zero runtime change — measured, not asserted

The claim is that the consumer's behaviour is untouched, so it is proven rather
than inferred from a passing suite. Transpiling the file before and after the
diff (TypeScript API, ES2022/ESNext, 0 diagnostics) emits byte-identical
JavaScript — 6686 bytes each, sha256
6a5c5a957517a0489699ff85054a5edd7409f8809d5fe0084ad96f2a16655e9d on both sides.
The 13 wording tests passing unchanged is corroboration, not the proof.

Reverse verification — the annotations are load-bearing

Predicted before running: reverting only the lineFor parameter, leaving the
Map key widened, must produce exactly one TS2345, and at the call site
inside emitWriteWarning rather than at the declaration, because byReason's key
is by then the four-arm union. Observed, after committing the fix so the revert
had a real restore point:

src/providers/writeWarningToast.ts(207,24): error TS2345: Argument of type
  '"readonly" | "readonly_when" | "primary_key" | "objectui:unrecognized-drop-reason"'
  is not assignable to parameter of type '"readonly" | "readonly_when" | "primary_key"'.
Exit status 2

One error, that code, that site. The mutation was confirmed on disk before the
run (injected text present once, removed text absent, Map key still widened as
a control), and the restore was proven by observed state rather than an exit
code: on-disk blob c4560fd222fd8f5d9d961ea3534793f57e45c165 equals
git rev-parse HEAD:<path>, with git diff HEAD and git status --porcelain
both empty. type-check returns to exit 0 on the restored tree.

Stale prose this PR created — now FIXED in this PR

The first round left two sentences in writeWarningToast.ts that this diff had
just made false. It reported them instead of editing them, because that order was
bounded to two annotations. The maintainer ruled option B on 2026-08-30
(comment 5468298926):
correcting prose this diff falsifies is finishing the change, not widening it —
"a stale comment about a type contract is the same defect class as the card."

Both are now corrected, in commit 3ec17deb, and nothing else in that file moved.

  1. strippedLineUnknownReason's docstring said the adapter "asserts the entry
    into DroppedFieldsEvent without ever checking the value against the spec
    enum". It now says the boundary PARSES reason against the spec enum and
    routes a value the enum does not name onto its explicit skew arm, whose
    UNRECOGNIZED_DROP_REASON is by construction not a key of the table above —
    so a server ahead of the pin still arrives at this line. Only the mechanism
    was stale; the conclusion it supports was true before and is true now.
  2. lineFor's docstring opened "The PARAMETER carries the spec union — that is
    what makes STRIPPED_LINE exhaustive-checked at its declaration above." The
    parameter now carries the two-arm notice union, and the causal half was
    already loose before this PR. It now says exhaustiveness has never come from
    this signature: it comes from that table's own Record declaration above
    being keyed by DroppedFieldsEvent['reason'].

Bounds honoured — checked, not asserted

  • STRIPPED_LINE's declaration did not move. Line 108, unchanged; still keyed
    by the SPEC union, which is writeWarningToast branches on DroppedFieldsEvent.reason with a binary ternary, so every future reason is labelled "Read-only" #3935's guarantee that a new spec arm fails
    type-check unworded.
  • The sentence the option-A ruling rests on is byte-identical and unmoved
    the one about the widened lookup and the undefined being reachable, not dead.
    Its three lines occur exactly once in both HEAD and the edited tree, and they
    appear in the diff as context lines, never as changed lines.
  • No code change. The diff is 8 added / 6 removed lines, every one of them a
    comment line in a single file.
  • No @object-ui/app-shell changeset entry, and the pre-existing changeset was
    not touched — check-changeset-overwrite reads "1 changeset(s) added, 0
    modified, 0 deleted", and check-changeset-presence is satisfied by the
    changeset this PR already carries.

Readings for this round (head 3ec17deb)

Run Reading
pnpm --filter @object-ui/app-shell type-check echoed tsc --noEmit && tsc -p tsconfig.test.json, exit 0
pnpm exec vitest run packages/app-shell/src/providers/writeWarningToast.test.ts Test Files 1 passed (1) / Tests 13 passed (13) — messages unchanged
check:i18n-keys exit 0 — "Every in-scope call-site key resolves against the en pack (2834 keys)"
check:i18n-drift exit 0 — "0 en value(s) changed"
check:i18n-dead-keys exit 0 — and it says of itself "This is a REPORT, not a gate"
check:control-bytes exit 0 — "scanned 5737 tracked text file(s)"
check-changeset-presence · -fixed · -no-major · -overwrite all exit 0
check:shell-escape-residue · check-type-check-coverage · check-lint-coverage all exit 0
eslint on the changed file exit 0, --format json: 1 file linted, 0 errors, 0 warnings

Every exit code above was captured before any pipe, and each row quotes the gate's
own verdict line rather than a bare $?.

The lint reading is a declared narrowing, not a repo scan. It carries the three
things a narrowing owes: the population came from eslint's own resolution (the file
resolves a config and is linted, not ignored — 1 file in the JSON report, not 0);
the count came from --format json; and the invariance holds because this config
is not type-aware--print-config shows parserOptions with no project and
no projectService, so no cross-file type information exists for a comment-only
edit in one module to move. The full repo lint is CI's run either way.

Gate families deliberately not re-run, with the reason: everything whose subject is
the export surface, the dist artifacts, the module graph or the registration
tables — spec-symbols, spec-floors, readme-exports, phantom-deps,
self-import, esm-specifiers, published-dist, vi-mock-specifiers. The
executable emit is proven unchanged (below), so none of them can move.

Correction to the ruling: "byte-identical emit" needs its comment mode stated

The ruling's ground for adding no changeset entry was that "the emitted JavaScript
is byte-identical ... The prose edits B adds cannot change that." Measured on this
round, that holds only with comments stripped, and this repo does not strip
them: tsconfig.base.json sets removeComments: false, and @object-ui/app-shell
builds with a bare tsc. Docstring bytes therefore do reach dist/index.js and
dist/index.d.ts.

Both modes were measured before and after the edit, each leg carrying a success
control
— 0 diagnostics, real JS emitted, three known markers present — because a
digest taken from a command that failed identically twice is a perfect false
identity proof, which is exactly the trap the last round caught itself in:

Emit mode Before After Verdict
comments stripped — executable content f9ff9b85 · 2181 B f9ff9b85 · 2181 B identical
comments kept — removeComments: false, the real build 6a5c5a95 · 6726 B 393f0180 · 6854 B differs

Filtering the comment-kept emit diff for changed lines that are not comment lines
returns nothing: every byte that moved is docstring prose. So the ruling's
conclusion survives on stronger ground than the phrase it used — zero executable
change
, with the only published bytes that move being documentation. Read it as
"byte-identical executable emit".

Footnote on the earlier number: the previous round's 6686 is this same emit
counted in UTF-16 code units; the file carries CJK and em-dashes, so its length in
bytes is 6726. The sha256 is unchanged and was reproduced here exactly —
6a5c5a957517a0489699ff85054a5edd7409f8809d5fe0084ad96f2a16655e9d.

What was verified against the tree rather than taken from the order

  • The dispatched line numbers (107 / 157 / 190) were re-derived on origin/main
    and matched exactly. After the added import they are 108 / 158 / 191.
  • The docstring quoted in the ruling was checked word for word against the tree
    before being repeated here; the ruling's rendering of lineFor's body had its
    generic arguments eaten by GitHub's body sanitizer, the tree has them.
  • The installed @objectstack/spec is 17.2.0 and
    DroppedFieldsEventSchema.shape.reason.options is exactly
    ["readonly","readonly_when","primary_key"].

Generated by Claude Code


Generated by Claude Code

`notifyDroppedFields` filtered a create/update response's `droppedFields` on
SHAPE alone — a hand-written `e is DroppedFieldsEvent` guard checking only
`Array.isArray(fields)` — so a `reason` outside the spec enum reached every
subscriber typed as though it were inside the union. A deployed client normally
runs behind the server it talks to, so a reason from the future is the expected
skew direction. `notifyBatchDroppedFields` did the same through its cast.

Both paths now read `reason` against `DroppedFieldsEventSchema.shape.reason`,
keep every entry, and route an unrecognized one to a named skew arm carrying the
wire value verbatim. The spec type stays the canonical arm — no widening to
`string`.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

ℹ️ Console Performance Budget — not measured

This run did not produce a console bundle to measure, so there is no pass/fail verdict for the performance budget.

This is not a budget violation. Nothing was measured — the numbers a real violation would carry are simply absent.

Step Outcome
Build packages failure
Check console performance budget skipped

See the workflow run for details.

No package size report: it is only generated from a complete package build, so a partial one is never shown.

os-sam commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

CI red on 09af7e1 — this is the declared fork, and the fix is in flight

Four checks failed. All four are one cause, and it is the one this PR opened by declaring
rather than a surprise:

check why
Type Check Failed: @object-ui/app-shell#type-check, exit 2 — the only failing task of 78 (9 cached)
Bundle Analysis needs a complete package build; app-shell's tsc build is the same failure
README Export Check reads built dist, which app-shell never produced
Doc Snippet Type Check same closure

Verified from the job log rather than inferred from the PR body: nothing else in the workspace
fails, which matches the measured 39-of-40 build exactly.

Worth noting that the repo's own budget job got this right where a cruder summary would not —
it reported "Console Performance Budget — not measured", naming Build packages: failure
and Check console performance budget: skipped, and stated plainly that "this is not a budget
violation. Nothing was measured."
That is the correct reading: a skipped measurement is not a
red one, and collapsing the two is how a real regression later gets waved through.

Not standing down — ruled and dispatched

The fork was reported per the 2026-08-22 ruling's own stop condition. I ruled it in
#4934 (comment): option A,
with the dispatched file surface amended to include
packages/app-shell/src/providers/writeWarningToast.ts, bounded to two type annotations
(lineFor's parameter and the byReason Map key). A patch round is running now. When it
lands, @object-ui/app-shell type-check should exit 0 and the workspace build should be 40 of
40 — and that, not an exit code, is the reading I asked for.

STRIPPED_LINE stays declared over DroppedFieldsEvent['reason'], so objectui#3935's
guarantee that a new spec arm fails type-check unworded is untouched by the fix.

⛔ No re-run is being spent on this. A re-run is for a failure that may not be this PR's; this
one is this PR's, is fully explained, and re-running it would just reproduce it.

One correction to this PR's body, coming with the patch

The body says clause ② does not fire because nothing was added to the published
@objectstack/spec type. That is too narrow. The gate judges "changes contract accept/reject
behaviour or widens the public surface"
from card content, and does not scope "public" to
the spec — objectui's own published surface counts, and
WriteWarningEvent['droppedFields'] going from DroppedFieldsEvent[] to the two-arm
DroppedFieldsNotice[] is exactly that. The broken consumer is the proof. Clause ② fires and
this PR's review is CONTRACT_REVIEW_TIER, not mine to close.
The accurate half of the
original claim stands: nothing was added to the spec type, and spec-symbol-batch6.test.ts's
pin that DroppedFieldsEvent IS the spec type still passes.


Generated by Claude Code

…union

The boundary now parses a write-strip's `reason` against the spec enum
(objectui#4934), so `WriteWarningEvent['droppedFields']` is the two-arm
`DroppedFieldsNotice[]` and a skew `reason` is deliberately NOT assignable to
`DroppedFieldsEvent['reason']`. Two annotations in `writeWarningToast.ts` were
pinned to the spec union and stopped compiling.

Both were already narrower than the file's own documented contract. `lineFor`
looks the reason up through a widened view of `STRIPPED_LINE`, and its docstring
says outright that the runtime value may sit outside that union — so the
`undefined` this branch handles is reachable, not dead. Only the parameter and
the `Map` key had been left on the union; this corrects them rather than
admitting a new case.

`STRIPPED_LINE` keeps its `Record<DroppedFieldsEvent['reason'], StrippedLine>`
declaration, so objectui#3935's guarantee — a new SPEC arm fails `type-check`
unworded — survives untouched. Widen the lookups, not the table.

Zero runtime change, measured rather than asserted: the emitted JavaScript is
byte-identical across the diff (tsc transpile, sha256 6a5c5a95…, 6686 bytes
each) and the toast's 13 wording tests pass unchanged.

The changeset now states the blast radius: a consumer branching exhaustively on
`reason` must widen its annotation, and that compile error is the intended
signal.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 45 chunks) 3177.0 KB 3222.7 KB
Main entry chunk (gzip) 143.6 KB 350 KB
Entry file index-C482dlez.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 12.46KB 4.71KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 512.13KB 116.43KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 174.02KB 48.28KB
fields (index.js) 243.36KB 61.51KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.92KB 12.93KB
plugin-charts (index.js) 64.68KB 18.35KB
plugin-chatbot (index.js) 190.53KB 45.18KB
plugin-dashboard (index.js) 133.48KB 34.51KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 245.43KB 62.46KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 133.32KB 32.69KB
plugin-gantt (index.js) 165.23KB 40.37KB
plugin-grid (index.js) 201.69KB 54.58KB
plugin-kanban (index.js) 53.14KB 14.64KB
plugin-list (index.js) 113.15KB 27.59KB
plugin-map (index.js) 20.20KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 28.95KB 8.33KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.83KB 21.11KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 76.75KB 25.49KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 3.11KB 1.48KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

os-sam commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

RULING on the stale-docstring question: B — the two sentences are corrected in this PR

And three corrections to my dispatch order, all of them the dev's and all of them right.

⛔ My order asked for a number that cannot exist

I ordered "the workspace build to 40 of 40, not 39." That denominator was wrong, and
wrong in an instructive way: app-shell#build has three direct dependents in the turbo
graph (console, example-console-starter, example-byo-backend-console). While app-shell
fails, those three are never attempted and never counted — so 39 successful, 40 total plus 3
uncounted is 43 successful, 43 total on a green tree. The denominator was itself a function
of the failure.
Had the dev obeyed me literally it would have chased 40/40 forever, or worse,
reported 43/43 as a miss.

Standing lesson, recorded against this seat: a task-count denominator read off a failing
build is not the denominator of a passing one.
When ordering a build reading, name the
condition ("zero Failed lines, every task attempted"), never a count carried over from the red
run.

My clue list was a floor and I did not say so

The order listed gates to re-run. The dev derived the actual set from the diff instead and
found four I had missed: check-changeset-overwrite (this round edits an existing
changeset) and the three i18n gates (writeWarningToast.ts is a wording table that
check:i18n-keys / i18n-drift / i18n-dead-keys read). All four green. Deriving the gate
set from the diff rather than from the dispatcher's list is the correct default — a clue list
is a floor, never a clearance, and I should have written it that way.

⭐ The discarded false proof is the most valuable thing in this report

The first attempt to prove byte-identical emit used pnpm exec esbuild, which is not
installed. Both legs exited 254 and both wrote the same 113-byte pnpm error text — so the
hashes matched, and it read as a clean proof of identity.
The dev caught it, discarded it as
NOT MEASURED, and redid it through the TypeScript API (both emits 6686 bytes, sha256
6a5c5a95…, with a non-empty source diff printed as the control).

This is a shape worth naming, because it is not on the standard NOT-MEASURED list: error
output is deterministic, so hashing the output of a command that failed identically twice
produces a perfect false identity proof.
Any A/B comparison by digest needs a control that
the command succeeded, not merely that the two digests agree. Adding it to the lane's list.

The ruling — B, narrowly

Both sentences this PR falsifies get corrected here:

  • strippedLineUnknownReason's docstring still says the adapter asserts the entry "without
    ever checking the value against the spec enum"
    — the boundary now checks it.
  • lineFor's docstring opens "The PARAMETER carries the spec union" — it no longer does.

Why my own two-annotation fence does not bar this. The fence existed to stop the fork
being absorbed — to stop a dev seat making design decisions in packages/app-shell under
cover of a bounded fix. It was never a rule that the PR may ship a sentence it has itself just
made false. Correcting prose this diff falsifies is finishing the change, not widening it.

And I have no room to rule otherwise, because I accepted the opposite reasoning today on
#6883: its central repair was exactly this — a comment carrying a justification measurement had
made false, left in place, which "the next triage checks, finds false, concludes 'burn it
down', and lands the exact regression this ledger exists to prevent."
A stale comment about a
type contract is the same defect class as the card. Shipping one knowingly, in the same commit
that creates it, is worse than the drift #6883 spent a PR removing.

四棱:① 长远合理性 —— B 让文件停止描述一个本 PR 刚删掉的边界,是"声明即真实"的最低要求;A 把已知
为假的句子留在树上,靠记忆去补。② 业务拉动 —— 弱,但这两句正是下一个 agent 读这条接缝时的第一手材料。
③ 防 AI 写错 —— 这是决定性的一棱:陈旧注释不是噪音,是被植入的前提,#6883 刚为此付了一整个 PR。
④ 不扩散 —— 两句话,文件已在本 diff 中打开,零行为改动、零门禁面。

Bounds on B — narrower than "rewrite the docstrings":

  1. Correct only what this diff falsifies. lineFor's causal claim was already loose before
    this PR (exhaustiveness comes from STRIPPED_LINE's own Record declaration, not from the
    parameter) — say the true thing, do not expand it into a general rewrite of the block.
  2. STRIPPED_LINE's declaration still does not move. Unchanged from the original ruling.
  3. The sentence the option-A ruling quotes as its evidence — "the runtime value may sit outside
    that union … the undefined this branch handles is therefore reachable, not dead"
    stays
    true and stays put.
    If a rewrite would touch it, stop and report.

No @object-ui/app-shell changeset entry — confirmed, and for the stated reason

Both edited sites are module-private (lineFor unexported, byReason a local) and the emitted
JavaScript is proven byte-identical, not assumed. The prose edits B adds cannot change that.
A type-only change with identical emit is not a published change; adding a patch entry would
declare a release that ships nothing.


Generated by Claude Code

… falsified

`strippedLineUnknownReason` still described the PRE-fix boundary — the adapter
"asserts the entry into `DroppedFieldsEvent` without ever checking the value
against the spec enum". It checks it now: `notifyDroppedFields` parses `reason`
and routes an unrecognized value to the named skew arm. The conclusion the
sentence supports is unchanged — `UNRECOGNIZED_DROP_REASON` is still not a key
of `STRIPPED_LINE`, so this fallback is still reachable — only the mechanism
was stale.

`lineFor`'s docstring opened "The PARAMETER carries the spec union", which it no
longer does, and hung `STRIPPED_LINE`'s exhaustiveness on that parameter. That
causal claim was already loose before this PR: exhaustiveness comes from the
table's own `Record<DroppedFieldsEvent['reason'], StrippedLine>` declaration,
never from this signature. Both are now stated truthfully.

Comments only, and bounded: `STRIPPED_LINE`'s declaration does not move, and the
sentence about the widened lookup being reachable rather than dead is untouched
to the byte. The comment-stripped transpile is identical before and after
(sha256 f9ff9b85b9cf39e28caf81a4bd99708bfc36b903b190af35fe56bca731e91bf8, both
sides, 0 diagnostics); every changed line of the comment-preserving emit is a
comment line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 45 chunks) 3177.0 KB 3222.7 KB
Main entry chunk (gzip) 143.6 KB 350 KB
Entry file index-C482dlez.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 12.46KB 4.71KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 512.13KB 116.43KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 174.02KB 48.28KB
fields (index.js) 243.36KB 61.51KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.92KB 12.93KB
plugin-charts (index.js) 64.68KB 18.35KB
plugin-chatbot (index.js) 190.53KB 45.18KB
plugin-dashboard (index.js) 133.48KB 34.51KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 245.43KB 62.46KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 133.32KB 32.69KB
plugin-gantt (index.js) 165.23KB 40.37KB
plugin-grid (index.js) 201.69KB 54.58KB
plugin-kanban (index.js) 53.14KB 14.64KB
plugin-list (index.js) 113.15KB 27.59KB
plugin-map (index.js) 20.20KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 28.95KB 8.33KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.83KB 21.11KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 76.75KB 25.49KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 3.11KB 1.48KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

os-sam commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Docstring round landed at 3ec17deb — and it corrects my ruling's stated ground

Verified independently before the report arrived: the commit touches one file, 8 insertions
/ 6 deletions
, and filtering the diff for changed lines that are not comment lines returns
nothing. STRIPPED_LINE's declaration did not move. The protected option-A evidence
sentence appears in the diff only as context, never as a changed line — the dev ended its
replacement text at the exact byte where that sentence begins, which is a cleaner way to
honour "stays put" than editing around it and hoping.

⛔ My ruling asserted something conditional as if it were unconditional

I wrote: "the emitted JavaScript is proven byte-identical, not assumed. The prose edits B
adds cannot change that."

That holds only with comments stripped. tsconfig.base.json:22 sets
removeComments: false, and app-shell builds with a bare tsc — so docstring bytes really do
reach dist/index.js and dist/index.d.ts, and the comment-preserving emit legitimately
differs (6726 B → 6854 B). I asserted a property of the build without checking the build's
flags; the dev checked them.

The conclusion survives, on better ground than the phrase I used:

mode before after
comments stripped (executable emit) f9ff9b85… / 2181 B f9ff9b85… / 2181 B identical
comments kept (the real build) 6a5c5a95… / 6726 B 393f0180… / 6854 B differs — and every differing line is a comment line

So: read my ruling as "byte-identical executable emit". No @object-ui/app-shell changeset
entry is still right, and the repo agrees rather than just me — check-changeset-presence
passes on its own terms: "3 source file(s) of 2 released package(s) changed, and this change
declares 1 changeset(s)"
, and check-changeset-overwrite reports "1 changeset(s) added, 0
modified, 0 deleted"
, which independently proves the existing changeset was not touched.

That is the third correction a dev has handed me today, and the pattern in all three is the
same: I stated a number or a property I had not measured (a build denominator, a file surface,
now a compiler flag), and the seat that measured caught it.

Two smaller corrections worth keeping

  • An inherited lane note is wrong. The 2026-08-26 note says every body PATCH downgrades
    the session-URL footer to the bare form. It did not here: the REST read-back was
    byte-identical to what was sent, session-form footer intact. The note should not be trusted
    as a standing fact without re-measuring.
  • "6686 bytes" from the previous round was UTF-16 code units, not bytes — this file carries
    CJK and em-dashes, so its byte length is 6726. The sha256 reproduced exactly, so both rounds
    measured the same artifact; only the unit was mislabelled.

Also worth noting the dev derived its gate set from objectui's own package.json and
.github/workflows rather than from my list or from objectstack's dispatch-gates.mjs — which
lives only in objectstack and would have answered about the wrong tree.

Next: clause-② review at CONTRACT_REVIEW_TIER

This PR changes contract accept/reject behaviour and widens objectui's published surface, so
its review is fable-tier and is not mine to close. Dispatching it now against head
3ec17deb. Landing stays gated on CI green on that head.


Generated by Claude Code

os-sam commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

ACCEPT WITH FOLLOW-UP

Clause-② contract review at CONTRACT_REVIEW_TIER, against head 3ec17debb46ec5d1dfdcba54dfadb1e0951df1d7 (card #4934). Reviewed read-only in a dedicated worktree at that SHA; nothing in this review edits, pushes, or merges. The follow-up items are prose-accuracy and hardening notes — none of them touches the accept/reject behaviour, the published shape, or any pinned guarantee, so they do not need to block landing once the PM has dispatched the one changeset sentence below (it can ride as one commit on this PR).

First, the jurisdictional question this seat exists for: the PM's clause-② reading is correct. The gate judges from card content and does not scope "public" to @objectstack/spec. This card changes contract accept/reject behaviour at the one boundary that reads the wire (reason is now parsed where it was asserted), and it widens objectui's own published surface — WriteWarningEvent['droppedFields'] goes from DroppedFieldsEvent[] to DroppedFieldsNotice[], and dist/index.d.ts now exports UNRECOGNIZED_DROP_REASON, UnrecognizedDropReasonEvent, and DroppedFieldsNotice (verified in the built artifact). The first round's "does not fire" was wrong for the reason the PM gave; the correction was right, and this review is the consequence.

1. The widening is the right shape — and the rejected alternatives were rightly rejected

DroppedFieldsEvent | UnrecognizedDropReasonEvent, discriminated by reason === UNRECOGNIZED_DROP_REASON, is faithful to the maintainer's clause 3 (named, documented skew arm): the arm is named, extensively documented at the declaration, carries the wire value verbatim in unrecognizedReason, and its reason is a literal type disjoint from the spec enum, so the union discriminates soundly.

I also swept for consumers independently: outside the boundary, its test, and writeWarningToast.ts, no code in packages/ or apps/ reads a dropped-fields reason. The card's blast-radius claim holds.

2. The sentinel is safe, and the safety is pinned rather than argued

'objectui:unrecognized-drop-reason' is namespaced into the consuming repo's own prefix; the spec's arms are bare snake_case (readonly, readonly_when, primary_key — read off the installed pin, 17.2.0, at review time). The argument "spec will not claim objectui's namespace" is sound but the PR does not rest on it: droppedFieldsReason.boundary.test.ts line 138 pins DroppedFieldsEventSchema.shape.reason.options NOT containing the sentinel, against the installed pin — so a future pin bump that ever did collide fails this repo's CI at the moment of the bump, which is the only moment the collision could be introduced. That pin is value-independent (it tests whatever the sentinel currently is), which is the stronger property. The collision failure mode — real reasons merging into the skew bucket — is therefore intercepted at the right place. Sound.

3. The lie is deleted; what over-claiming remains is pre-existing and smaller — noted for a follow-up card

The original defect — e is DroppedFieldsEvent asserting a parsed reason that was never read — is gone. The filter now asserts WireDroppedFieldsEntry, whose reason is unknown; reason acquires a type only through isRecognizedDropReason, whose accept set is derived from DroppedFieldsEventSchema.shape.reason.options (not restated). The one remaining cast, entry as DroppedFieldsEvent, sits strictly after that parse; the batch path's entry as WireDroppedFieldsEntry & { index?: number } keeps reason unparsed until asDroppedFieldsNotice. All verified in the diff and the head tree.

Two smaller over-claims survive, both pre-existing and outside this card's scope: the guard checks Array.isArray(fields) && fields.length > 0 but WireDroppedFieldsEntry (via Omit<DroppedFieldsEvent, 'reason'>) still asserts fields: string[] element types unchecked, and asserts object: string as present unchecked (the spec schema declares object: z.ZodString, required — note the skew arm honestly declares object?: string instead). The old guard asserted all of this plus the reason lie; this PR shrank the over-claim to exactly what it inherited. Worth an observation-class card against the same seam, not a change to this PR.

4. minor is right — by this repo's own written policy, not just by argument

check-changeset-no-major.mjs's header states the policy in as many words: every publishable package sits in one fixed group whose major is pinned to @objectstack's major, and "objectui's own breaking changes ship as minor with the break spelled out in the changeset body." This changeset does exactly that: a full paragraph declaring that an exhaustive reason consumer stops compiling with TS2345, that the error IS the intended and only signal, the migration (DroppedFieldsNotice['reason'], narrow via the sentinel, read unrecognizedReason), and the widen-the-lookups-not-the-table guidance protecting #3935. That is an honest declaration of what external consumers will feel. minor correct; declaration honest — with one exception, which is the follow-up (below).

5. objectui#3935 is intact

STRIPPED_LINE remains Record<DroppedFieldsEvent['reason'], StrippedLine> at line 108 of writeWarningToast.ts, keyed by the SPEC union; it appears in the PR diff only as hunk context, never as a changed line. The widening names DroppedFieldsNotice['reason'] only at the two lookup sites (lineFor's parameter, the byReason Map key). The sentinel is by construction not a key of the table, so the skew path lands on strippedLineUnknownReason through the already-documented widened lookup. A NEW spec arm still fails type-check unworded at the table's declaration. Confirmed in the head tree, not just the diff.

6. Evidence — what I reproduced, what CI corroborates, what I could not replay

Reproduced independently in a clean worktree at 3ec17deb (fresh install, closures built first — my own first type-check hit TS6305 from an unbuilt closure and was discarded as NOT MEASURED, then re-run green after the build, per the same discipline this PR practices):

  • Boundary tests: 7/7 pass. Consumer tests: 13/13 pass, including the skew-path wording test. spec-symbol-batch6: 13/13.
  • @object-ui/data-objectstack type-check: exit 0 — and its tsconfig includes src/**/*, so the @ts-expect-error assignability pin runs inside an enforced program, not as decoration.
  • @object-ui/app-shell type-check: exit 0, zero TS errors — the fork fix compiles.
  • Emit identity, reproduced byte-for-byte with success controls (0 diagnostics, real output, known markers present, on every leg): comments-stripped emit f9ff9b85…/2181 B identical across base → c01e5a23ec17deb; comments-kept emit 6a5c5a95…/6726 B identical base → c01e5a2 (so the annotation round truly changed no emitted byte, comments included), then 393f0180…/6854 B at 3ec17deb. I separately verified every changed line of the 3ec17deb diff is a comment line (8+/6−, one file, zero non-comment changed lines). The PR's hashes are real.
  • dist markers: grep -c UNRECOGNIZED_DROP_REASON on the built dist gives 5 (index.d.ts) and 3 (index.js) — exactly as claimed.
  • Workspace build: pnpm exec turbo run build --filter='!@object-ui/site' in my worktree — Tasks: 43 successful, 43 total, zero Failed lines, exit 0. The 43/43 reading reproduces.
  • CI on head 3ec17deb: all 30 check runs green (Type Check, Lint, 4 test shards, Build & E2E, README Export Check, Bundle Analysis, all changeset gates; only dependabot/coverage skipped by design) — independent corroboration.

Not replayable read-only, stated as such:

  • The red-first run. The reported red output (3 failed / 3 passed of 6) is exactly what the pre-fix code must produce against these assertions — notice.reason arriving as 'some_future_reason' verbatim, "reason": 42 in the diff — and the three pre-fix passes are precisely the discrimination controls. But the shipped file has 7 tests; the compile-time assignability pin cannot have existed pre-fix (it imports UNRECOGNIZED_DROP_REASON), so it was necessarily added after the red run. Plausible and internally consistent; not independently reproduced, since doing so requires mutating the tree.
  • The PM's 43-vs-40 correction is arithmetically right and the standing lesson ("a denominator read off a failing build is not the denominator of a passing one") is worth keeping.

The FOLLOW-UP: one sentence in the changeset is now stale — the same defect class this PR just paid to correct

The changeset (.changeset/4934-dropped-fields-reason-boundary.md) says of app-shell: "Its emitted JavaScript is byte-identical and its wording tests pass unchanged." That was fully true when written (at c01e5a2, both emit modes identical — I verified). The docstring commit 3ec17deb then falsified its comment-kept half: removeComments: false is set, app-shell builds with bare tsc, so docstring bytes reach dist and the real build's emit now differs (6a5c5a95393f0180). The PR body and the PM's own correction both caught this for the ruling's phrasing — "read it as byte-identical executable emit" — but the sibling sentence in the changeset, which is a release-notes input external consumers read, kept the unqualified form. By the option-B ruling's own standard ("a stale comment about a type contract is the same defect class as the card"), this sentence should get the same one-word repair: qualify it to "byte-identical executable emit" (or "no executable change"). One line, in a changeset this PR owns (it did not exist at the merge base, so check-changeset-overwrite is indifferent), ridable as a single commit here.

Secondary follow-up notes, none blocking:

  1. The residual guard over-claims from §3 (fields element types, object presence) — observation-class card against the boundary, if the seam gets another pass.
  2. A trivial, arguably beneficial runtime delta nobody has stated: pre-fix, two distinct unknown wire reasons would bucket separately in the toast (two identical cause-free lines); post-fix both collapse onto the sentinel bucket (one line, fields merged). Empty population today, wording tests unaffected — recording it so the "zero runtime change in the consumer" claim is read as what it is (true of the consumer's code and emit; the boundary's data shaping is the change, and it is declared).
  3. The red-first 6-vs-7 test count above — no action needed; future red-first reports would be stronger if the red-run test list were named so the delta is explicit.

Verdict

The boundary now enforces what it declares, the skew is loud instead of lied about, every ruling's bound was honoured and verified (entries kept; canonical arm preserved; #3935 untouched; protected sentence unmoved, context-only in the diff), the evidence reproduces byte-for-byte where I could reproduce it, and CI is green on the head. ACCEPT WITH FOLLOW-UP as stated: land after the one-sentence changeset qualification (or with the PM's explicit decision to ship the changeset as-is, recorded on this PR — my recommendation is the one-liner).


Generated by Claude Code

os-sam commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Clause-② review received: ACCEPT WITH FOLLOW-UP. Acting on the follow-up; not closing this myself.

The contract-tier review is in (comment 5468453319) and its follow-up is one I should have caught
and did not.

The follow-up is the sharpest catch of the round

Three of us looked at the "byte-identical emit" claim today — the dev that measured it, the
reviewer, and me — and all three of us fixed the wrong copy of it. The dev corrected my
ruling's phrasing; I corrected it again in a comment; and the identical sentence sitting in
.changeset/4934-dropped-fields-reason-boundary.md:56 went untouched:

two type annotations, no runtime change. Its emitted JavaScript is byte-identical

Verified on the current head before acting: it is there, unqualified, on line 56 of a 60-line
file.

And the changeset is the copy that actually matters. The PR body and my ruling are thread
artifacts — wrong there, a reader loses a moment. The changeset is a release-notes input: it
compiles into published notes and reaches people who will never see this conversation. So the
one place the claim needed to be right is the one place nobody fixed it.

That is worth naming as a rule rather than a slip: when a claim turns out to be wrong, fix
every copy of it, and fix the most durable copy first.
Today's ordering was exactly inverted —
we repaired the ephemeral copies and left the shipping one.

A one-commit correction is dispatched: qualify it to the executable emit, which is what the
measurement actually supports (comments-stripped f9ff9b85… identical both sides; the
comment-preserving build differs only on comment lines, because removeComments: false).
⛔ Still no @object-ui/app-shell changeset entry — that has been ruled twice and the reasoning
is unchanged.

On the rest of the review

The reviewer reproduced, rather than accepted, the load-bearing evidence — boundary 7/7,
consumer 13/13, both type-checks exit 0, turbo 43 successful, 43 total, dist markers 5/3, and
all three emit digests byte-for-byte with success controls. It also established something
the PR only argued: string-widening would have failed the existing spec-symbol-batch6 pin, so
objectui#3160's canonical arm is enforced, not merely asserted.

Its third finding — that WireDroppedFieldsEntry still over-claims fields element types and
object presence beyond what the shape filter checks — is correctly scoped as pre-existing,
strictly smaller than the lie this PR deletes, and observation-card material rather than a
change to this PR. I am filing it as its own card so it is not lost with the thread.

Its stated non-verifiables are honest and I am not treating them as gaps: the red-first run
cannot be replayed without mutating the tree, and the 6-vs-7 test count between the red run and
the shipped file is explained by the compile-time pin necessarily post-dating red.

CI is 30/30 green on 3ec17deb. Once the changeset correction lands and CI is green on the new
head, this is ready — ⛔ but the close is the contract tier's, not mine.


Generated by Claude Code

The changeset said of the app shell "Its emitted JavaScript is byte-identical".
That was true when written, at `c01e5a2` — both emit modes were identical there.
The docstring commit `3ec17deb` falsified its comment-kept half:
`tsconfig.base.json:22` sets `removeComments: false` and `@object-ui/app-shell`
builds with a bare `tsc`, so docstring bytes do reach `dist/index.js` and
`dist/index.d.ts`, and the real build's emit differs (6726 B -> 6854 B, sha256
`6a5c5a95...` -> `393f0180...`), every differing line a comment line.

The EXECUTABLE emit is unchanged and measured so: comments-stripped,
sha256 `f9ff9b85...` / 2181 B on both sides. One word, so the sentence says that.

This one is worth a commit where the identical slip in the PR body and in the
ruling was answered with a comment: a changeset is a release-notes input. It is
compiled into published notes and read by consumers who cannot see this thread,
so an unqualified claim here becomes the record rather than costing a reader a
moment.

Bump level, structure and every other line are untouched, and no
`@object-ui/app-shell` entry is added: the executable emit is unchanged, and a
comment-only difference in dist is not a published behaviour change.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_013hfmP9hoMd3dJwTh85J4yB
@github-actions

Copy link
Copy Markdown
Contributor

✅ Console Performance Budget

Metric Value Budget
Eager closure (gzip, 45 chunks) 3177.0 KB 3222.7 KB
Main entry chunk (gzip) 143.6 KB 350 KB
Entry file index-C482dlez.js
Status PASS

The eager closure is every chunk the entry reaches through static imports — what the browser fetches and parses before the app renders. The entry chunk on its own is a small fraction of it.


📦 Bundle Size Report

Package Size Gzipped
app-shell (consoleActionDispatch.js) 0.20KB 0.19KB
app-shell (index.js) 12.46KB 4.71KB
app-shell (runtime-config.js) 20.61KB 7.35KB
app-shell (types.js) 0.01KB 0.04KB
app-shell (urlParams.js) 10.06KB 3.86KB
auth (ActiveOrganizationStorage.js) 25.05KB 9.16KB
auth (AuthContext.js) 0.31KB 0.24KB
auth (AuthGuard.js) 2.07KB 1.00KB
auth (AuthProvider.js) 40.18KB 10.59KB
auth (AuthShell.js) 3.49KB 1.40KB
auth (ForgotPasswordForm.js) 12.21KB 3.45KB
auth (LoginForm.js) 18.15KB 5.39KB
auth (PreviewBanner.js) 0.90KB 0.50KB
auth (RegisterForm.js) 6.65KB 2.22KB
auth (SocialSignInButtons.js) 9.61KB 3.89KB
auth (UserMenu.js) 3.41KB 1.23KB
auth (auth-gate-events.js) 1.29KB 0.66KB
auth (authStyles.js) 5.04KB 1.72KB
auth (createAuthClient.js) 40.21KB 10.80KB
auth (createAuthenticatedFetch.js) 8.46KB 3.43KB
auth (index.js) 3.19KB 1.44KB
auth (invitation-status.js) 1.22KB 0.70KB
auth (org-roles.js) 6.66KB 2.78KB
auth (phone-identifier.js) 1.11KB 0.66KB
auth (types.js) 0.59KB 0.35KB
auth (useAuth.js) 5.30KB 1.02KB
auth (useWorkspaceAdminStatus.js) 5.13KB 2.35KB
collaboration (CommentThread.js) 26.08KB 7.56KB
collaboration (LiveCursors.js) 3.17KB 1.27KB
collaboration (PresenceAvatars.js) 6.49KB 2.64KB
collaboration (PresenceProvider.js) 2.79KB 1.13KB
collaboration (index.js) 1.68KB 0.73KB
collaboration (useCollaborationTranslation.js) 6.05KB 2.52KB
collaboration (useCommentSearch.js) 1.98KB 0.88KB
collaboration (useConflictResolution.js) 7.75KB 1.86KB
collaboration (useMentionNotifications.js) 1.81KB 0.68KB
collaboration (usePresence.js) 6.33KB 1.84KB
collaboration (useRealtimeSubscription.js) 7.91KB 2.01KB
components (index.js) 512.13KB 116.43KB
core (index.js) 5.30KB 2.13KB
create-plugin (index.js) 10.08KB 3.26KB
data-objectstack (index.js) 174.02KB 48.28KB
fields (index.js) 243.36KB 61.51KB
i18n (LocalizationContext.js) 1.76KB 0.96KB
i18n (currency.js) 1.22KB 0.64KB
i18n (fallbackInterpolation.js) 6.25KB 2.77KB
i18n (i18n.js) 4.28KB 1.75KB
i18n (index.js) 3.44KB 1.39KB
i18n (pickLocalized.js) 7.62KB 3.26KB
i18n (provider.js) 26.89KB 9.04KB
i18n (useDisplayLocale.js) 2.85KB 1.45KB
i18n (useObjectLabel.js) 33.40KB 8.71KB
i18n (useSafeTranslation.js) 5.60KB 2.33KB
layout (index.js) 38.95KB 10.97KB
mobile (MobileProvider.js) 0.92KB 0.49KB
mobile (ResponsiveContainer.js) 0.94KB 0.38KB
mobile (breakpoints.js) 1.51KB 0.70KB
mobile (createOfflineDataSource.js) 5.61KB 1.75KB
mobile (index.js) 1.55KB 0.62KB
mobile (offlineQueue.js) 3.91KB 1.35KB
mobile (pwa.js) 0.97KB 0.49KB
mobile (serviceWorker.js) 1.48KB 0.62KB
mobile (serviceWorkerSource.js) 3.41KB 1.48KB
mobile (useBreakpoint.js) 1.54KB 0.65KB
mobile (useGesture.js) 6.96KB 1.98KB
mobile (useOfflineSync.js) 1.99KB 0.72KB
mobile (usePullToRefresh.js) 2.53KB 0.85KB
mobile (useResponsive.js) 0.72KB 0.42KB
mobile (useResponsiveConfig.js) 1.37KB 0.63KB
mobile (useSpecGesture.js) 4.32KB 1.64KB
mobile (useTouchTarget.js) 1.01KB 0.54KB
permissions (MePermissionsProvider.js) 11.71KB 4.29KB
permissions (PermissionContext.js) 0.31KB 0.25KB
permissions (PermissionGuard.js) 0.89KB 0.45KB
permissions (PermissionProvider.js) 6.24KB 2.16KB
permissions (discardProofCache.js) 1.04KB 0.55KB
permissions (evaluator.js) 5.12KB 1.74KB
permissions (index.js) 0.93KB 0.41KB
permissions (store.js) 0.91KB 0.42KB
permissions (useFieldPermissions.js) 1.28KB 0.53KB
permissions (usePermissions.js) 4.83KB 2.27KB
plugin-ai (index.js) 15.75KB 3.80KB
plugin-calendar (index.js) 46.92KB 12.93KB
plugin-charts (index.js) 64.68KB 18.35KB
plugin-chatbot (index.js) 190.53KB 45.18KB
plugin-dashboard (index.js) 133.48KB 34.51KB
plugin-designer (index.js) 212.87KB 43.19KB
plugin-detail (index.js) 245.43KB 62.46KB
plugin-editor (index.js) 2.46KB 1.10KB
plugin-form (index.js) 133.32KB 32.69KB
plugin-gantt (index.js) 165.23KB 40.37KB
plugin-grid (index.js) 201.69KB 54.58KB
plugin-kanban (index.js) 53.14KB 14.64KB
plugin-list (index.js) 113.15KB 27.59KB
plugin-map (index.js) 20.20KB 6.66KB
plugin-markdown (index.js) 13.72KB 4.69KB
plugin-report (index.js) 43.51KB 11.94KB
plugin-timeline (index.js) 28.95KB 8.33KB
plugin-tree (index.js) 9.00KB 3.08KB
plugin-view (index.js) 85.83KB 21.11KB
providers (DataSourceProvider.js) 0.75KB 0.39KB
providers (MetadataProvider.js) 1.37KB 0.59KB
providers (ThemeProvider.js) 1.90KB 0.85KB
providers (UploadProvider.js) 11.66KB 3.50KB
providers (index.js) 0.45KB 0.23KB
providers (types.js) 0.01KB 0.04KB
react-runtime (index.js) 5.62KB 2.34KB
react (LazyPluginLoader.js) 4.47KB 1.63KB
react (SchemaRenderer.js) 76.75KB 25.49KB
react (data-invalidation.js) 5.05KB 2.08KB
react (index.js) 3.11KB 1.48KB
react (schema-input.js) 2.32KB 1.24KB
react (spec-input.js) 0.20KB 0.18KB
sdui-parser (codegen.js) 5.41KB 2.34KB
sdui-parser (dashboard-widget-options.js) 3.08KB 1.30KB
sdui-parser (index.js) 4.93KB 2.24KB
sdui-parser (input-type.js) 2.84KB 1.40KB
sdui-parser (parse.js) 20.57KB 5.88KB
sdui-parser (provenance.js) 3.66KB 1.82KB
sdui-parser (types.js) 0.28KB 0.23KB
sdui-parser (validate.js) 10.35KB 3.60KB
types (ai.js) 0.20KB 0.17KB
types (api-types.js) 0.20KB 0.18KB
types (app.js) 2.87KB 0.99KB
types (base.js) 0.20KB 0.18KB
types (blocks.js) 0.20KB 0.18KB
types (complex.js) 2.74KB 1.41KB
types (crud.js) 0.20KB 0.18KB
types (dashboard-filter-alias.js) 6.23KB 2.74KB
types (data-display.js) 3.75KB 1.85KB
types (data-protocol.js) 0.20KB 0.19KB
types (data.js) 0.20KB 0.18KB
types (designer.js) 1.85KB 0.85KB
types (disclosure.js) 0.20KB 0.18KB
types (error-code.js) 1.54KB 0.88KB
types (feedback.js) 0.20KB 0.18KB
types (field-types.js) 0.20KB 0.18KB
types (form.js) 0.20KB 0.18KB
types (http-inflight.js) 8.87KB 3.73KB
types (http-retry.js) 4.32KB 2.02KB
types (icon-key-migration.js) 4.26KB 1.63KB
types (index.js) 4.72KB 2.24KB
types (layout.js) 0.20KB 0.18KB
types (managed-by.js) 0.19KB 0.18KB
types (mobile.js) 2.59KB 1.31KB
types (navigation.js) 0.20KB 0.18KB
types (objectql.js) 0.20KB 0.18KB
types (overlay.js) 0.20KB 0.18KB
types (permissions.js) 0.20KB 0.18KB
types (plugin-scope.js) 0.20KB 0.18KB
types (record-components.js) 0.20KB 0.19KB
types (record-semantics.js) 1.28KB 0.67KB
types (registry.js) 0.20KB 0.18KB
types (reports.js) 0.20KB 0.18KB
types (spec-report.js) 5.05KB 1.93KB
types (spec-ui-namespace.js) 0.20KB 0.19KB
types (system-fields.js) 3.33KB 1.54KB
types (theme.js) 6.28KB 2.87KB
types (ui-action.js) 3.40KB 1.71KB
types (views.js) 0.20KB 0.18KB
types (widget.js) 0.20KB 0.18KB

Size Limits

  • ✅ Core packages should be < 50KB gzipped
  • ✅ Component packages should be < 100KB gzipped
  • ⚠️ Plugin packages should be < 150KB gzipped

os-sam commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

FOLLOW-UP DISCHARGED — the conditional ACCEPT of my review is now unconditional

Verified against head 2b8b48f82d71e7262a2a2efed8e6a196523c0714, read directly, not taken from the report:

  1. The repair is the right one and the paragraph is now true without thread context. The inter-head diff is exactly one file, one line — "Its emitted JavaScript" → "Its executable JavaScript" in .changeset/4934-dropped-fields-reason-boundary.md — and 2b8b48f8 is a single commit whose parent is the reviewed head 3ec17deb (ancestry checked; no rebase, no history rewrite). Read whole, the paragraph's claims each match a measurement I reproduced at review time: "executable JavaScript is byte-identical" is the f9ff9b85…/2181 B identity across base → head (the toast file is blob-identical between the two heads, so the digest carries over); "wording tests pass unchanged" is the reproduced 13/13; "two type annotations" describes the blast radius, which the comment-only docstring commit does not enlarge.
  2. No fourth copy. Swept all four changed files at 2b8b48f8 for sibling claims: the changeset's only other runtime claim ("Runtime wording is unchanged") is true and test-pinned; the two byte-identical hits in packages/data-objectstack/src/index.ts are pre-existing prose about DELETE requests, unrelated; the boundary test's "byte-identical to the wire entry" is about canonical-arm passthrough and is enforced by the very test that says it. The PR body's first-round claim is explicitly time-scoped to c01e5a2, where I verified it held in both emit modes. Nothing else needs the qualifier.
  3. CI is green on 2b8b48f8 and the review's evidence stands. All 30 check runs completed: 27 success, 3 skipped by design (dependabot, coverage placeholders), zero failures — Type Check, Lint, and all four test shards included, so the reading is complete, not a partial snapshot. Every reading I reproduced at review time (tests, type-checks, emit digests, dist markers, the 43/43 build) was computed from files that are blob-identical between the reviewed head and this one; the sole changed file is a changeset, which nothing compiles or imports.

No open conditions remain from this seat. Landing is the PM's call, as before.


Generated by Claude Code

@os-sam
os-sam marked this pull request as ready for review August 30, 2026 12:00
@os-sam
os-sam added this pull request to the merge queue Aug 30, 2026

os-sam commented Aug 30, 2026

Copy link
Copy Markdown
Collaborator

Landing — clause-② verdict rendered and discharged, CI 30/30 on 2b8b48f8

All three gates are satisfied and each by the right authority:

gate by whom state
Clause-② contract review CONTRACT_REVIEW_TIER, not this seat ACCEPT WITH FOLLOW-UP → FOLLOW-UP DISCHARGED
CI on the current head GitHub 30/30 — 27 success, 3 skipped by design, 0 failures
Governed surface repo's own Queue Guard success

Flipping ready and enabling auto-merge. To be explicit about the jurisdiction, since I said
repeatedly this was "not mine to close": the gate was that the review be at contract tier.
It was, its verdict is ACCEPT, and its condition is discharged — so this is execution of that
verdict, not a substitute for it. I did not review this PR and I am not approving it.

The discharge itself was better than a rubber stamp

The reviewer verified rather than accepted, on the point I would most likely have inferred: it
established that its earlier evidence still applies by showing every file that evidence was
computed from is blob-identical between the two heads
— carry-over as a reading, not an
inference — and it checked the ancestry to confirm 2b8b48f8 is a single commit on the head it
reviewed, not a rebase.

It also swept for a fourth copy of the "byte-identical" claim and found none, which is the
part that mattered: index.ts's two hits are pre-existing prose about DELETE requests, the
boundary test's copy is about wire-entry passthrough and is enforced by the test itself, the PR
body's version is explicitly time-scoped to c01e5a2 where it did hold in both emit modes, and
the changeset's only other runtime claim — "Runtime wording is unchanged" — is true and
test-pinned. That last finding also closes the open question I was holding: secondary
follow-up 2 (the two-distinct-unknown-reasons bucket collapse) needs no changeset line.
It is
recorded in the review, the sentence beside it is accurate, and no consumer-visible claim
overstates.

What this card took, and what it cost to get right

Card #4934's fix was ruled on 2026-08-22 and is four commits: the boundary parse, the two-line
consumer widening the ruled shape entailed, two docstrings the diff itself falsified, and one
word in a changeset. Three of those four exist because someone measured instead of complying —
the dev that found the fork and stopped, the dev that found removeComments: false, and the
reviewer that found the release-notes copy nobody had fixed.

Follow-ups filed rather than absorbed: objectui#6889 (the pre-existing WireDroppedFieldsEntry
over-claims), and the reviewer's note that a future red-first report should name its red-run
test list so a 6-vs-7 count delta is explicit rather than reconstructed.


Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants